/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Decimal } from "decimal.js"
import * as _ from "lodash"
import { PageList } from "../data"
import httpClient from "../httpclient"
import { Payment } from "../payment"
import { Address } from "../shipping"
import { OrderPayment } from "./payment"

export * from "./payment"

export class ShipmentItem {
  public id: string
  private product_id: number
  private variant_id: number
  public quantity: number
  public name: string
  private image_url: string

  public get productId() {
    return this.product_id
  }

  public set productId(productId) {
    this.product_id = productId
  }

  public get variantId() {
    return this.variant_id
  }

  public set variantId(variantId) {
    this.variant_id = variantId
  }

  public get imageUrl() {
    return this.image_url
  }

  public set imageUrl(imageUrl) {
    this.image_url = imageUrl
  }

}

export class Shipment {
  public id: string
  public consignor: string
  private shipping_provider: string
  private tracking_carrier: string
  private shipping_method: string
  private tracking_number: string
  private order_id: string
  public items: ShipmentItem[] = []
  private shipped_time: string
  private image_urls: string

  public containsItem(item: OrderItem): boolean {
    return _.find(this.items, aItem => aItem.productId === item.productId && aItem.variantId === item.variantId) !== undefined
  }

  public get shippingProvider() {
    return this.shipping_provider
  }

  public set shippingProvider(shippingProvider) {
    this.shipping_provider = shippingProvider
  }

  public get trackingCarrier() {
    return this.tracking_carrier
  }

  public set trackingCarrier(trackingCarrier) {
    this.tracking_carrier = trackingCarrier
  }

  public get shippingMethod() {
    return this.shipping_method
  }

  public set shippingMethod(shippingMethod) {
    this.shipping_method = shippingMethod
  }

  public get trackingNumber() {
    return this.tracking_number
  }

  public set trackingNumber(trackingNumber) {
    this.tracking_number = trackingNumber
  }

  public get orderId() {
    return this.order_id
  }

  public set orderId(orderId) {
    this.order_id = orderId
  }

  public get shippedTime() {
    return this.shipped_time
  }

  public set shippedTime(shippedTime) {
    this.shipped_time = shippedTime
  }

  public get imageUrls() {
    return this.image_urls
  }
}

export class ShipmentRequest {
  public id: string
  public shipping_provider?: string
  public shipping_method?: string
  public tracking_number?: string

  public set shippingProvider(shippingProvider: string) {
    this.shipping_provider = shippingProvider
  }

  public set shippingMethod(shippingMethod: string) {
    this.shipping_method = shippingMethod
  }

  public set trackingNumber(trackingNumber: string) {
    this.tracking_number = trackingNumber
  }
}

function assignOrderShipment(shipment: Shipment) {
  return _.assign(new Shipment(), shipment, {
    items: _.map(shipment.items, item => _.assign(new OrderItem(), item)),
  })
}

function assignOrderRefund(refund: OrderRefund) {
  return _.assign(new OrderRefund(), refund, {
    items: _.map(refund.items, item => _.assign(new OrderRefundItem(), item)),
  })
}


export enum OrderSource {
  Browser = "browser", Other = "other"
}

export enum OrderStatus {
  Pending = "pending",
  AwaitingPayment = "awaiting_payment",
  AwaitingFulfillment = "awaiting_fulfillment",
  AwaitingShipment = "awaiting_shipment",
  PartiallyShipped = "partially_shipped",
  Shipped = "shipped",
  //
  AwaitingRefund = "awaiting_refund",
  PartiallyRefunded = "partially_refunded",
  Refunded = "refunded",
  //
  AwaitingPickup = "awaiting_pickup",
  Cancelled = "cancelled",
  Closed = "closed",
  Declined = "declined",
  Completed = "completed",
  Disputed = "disputed",
}

export class OrderItem {
  public id: string
  private store_id: string
  private product_id: number
  private variant_id: number
  public quantity: number
  public price: number
  public amount: number
  public name: string
  public option_values: string
  private image_url: string
  private refunded_amount: number
  private refunding_amount: number
  private shipping_cost: number
  private discount_shipping_cost: number
  private discount_amount: number
  private total_price: number
  private subtotal_amount: number
  private total_amount: number

  public get storeId() {
    return this.store_id
  }

  public set storeId(storeId) {
    this.store_id = storeId
  }

  public get productId() {
    return this.product_id
  }

  public set productId(productId) {
    this.product_id = productId
  }

  public get variantId() {
    return this.variant_id
  }

  public set variantId(variantId) {
    this.variant_id = variantId
  }

  public get imageUrl() {
    return this.image_url
  }

  public set imageUrl(imageUrl) {
    this.image_url = imageUrl
  }

  public get optionValues() {
    return this.option_values
  }

  public set optionValues(values) {
    this.option_values = values
  }

  public get refundedAmount() {
    return this.refunded_amount
  }

  public set refundedAmount(refundedAmount) {
    this.refunded_amount = refundedAmount
  }

  public get refundingAmount() {
    return this.refunding_amount
  }

  public set refundingAmount(refundingAmount) {
    this.refunding_amount = refundingAmount
  }

  public get shippingCost() {
    return this.shipping_cost
  }

  public set shippingCost(cost) {
    this.shipping_cost = cost
  }

  public get discountShippingCost() {
    return this.discount_shipping_cost
  }

  public set discountShippingCost(cost) {
    this.discount_shipping_cost = cost
  }

  public get discountAmount() {
    return this.discount_amount
  }

  public set discountAmount(amount) {
    this.discount_amount = amount
  }

  public get subtotalAmount() {
    return this.subtotal_amount
  }

  public get totalPrice() {
    return this.total_price
  }

  public get totalAmount() {
    return this.total_amount
  }

  public getAmount(): number {
    return Decimal.mul(this.price, this.quantity).toNumber()
  }

}

export class Order {
  public id: string
  public status: OrderStatus
  private staff_notes: string
  public items: OrderItem[] = []
  private store_id: string
  private store_name: string
  public source: OrderSource
  public shipping_address: Address = new Address()
  public shipments: Shipment[] = []
  public refunds: OrderRefund[]
  private placing_expires: number = 0
  private placing_expired_time: string
  private payment_id ?: string
  private payment_status ?: string
  private payment_method ?: string
  private cancel_reason ?: string
  private close_reason ?: string
  private total_price: number
  private subtotal_amount: number
  private total_amount: number
  private total_shipping_cost: number
  private total_discount_amount: number
  private total_discount_shipping_cost: number

  private placed_time: string
  private paid_time: string
  private shipped_time: string
  private cancelled_time: string
  private closed_time: string

  public get shippingAddress() {
    return this.shipping_address
  }

  public set shippingAddress(address) {
    this.shipping_address = address
  }

  public set storeId(storeId) {
    this.store_id = storeId
  }

  public get storeId() {
    return this.store_id
  }

  public set storeName(storeName) {
    this.store_name = storeName
  }

  public get storeName() {
    return this.store_name
  }

  public set staffNotes(staffNotes) {
    this.staff_notes = staffNotes
  }

  public get staffNotes() {
    return this.staff_notes
  }

  public get placingExpires() {
    return this.placing_expires
  }

  public get placingExpiredTime() {
    return this.placing_expired_time
  }

  public get paymentId() {
    return this.payment_id
  }

  public get paymentStatus() {
    return this.payment_status
  }

  public get paymentMethod() {
    return this.payment_method
  }

  public get totalShippingCost() {
    return this.total_shipping_cost
  }

  public get totalDiscountAmount() {
    return this.total_discount_amount
  }

  public get totalDiscountShippingCost() {
    return this.total_discount_shipping_cost
  }

  public get subtotalAmount() {
    return this.subtotal_amount
  }

  public get totalAmount() {
    return this.total_amount
  }

  public get totalPrice() {
    return this.total_price
  }

  public get cancelReason() {
    return this.cancel_reason
  }

  public get closeReason() {
    return this.close_reason
  }

  public get placedTime() {
    return this.placed_time
  }

  public get paidTime() {
    return this.paid_time
  }

  public get shippedTime() {
    return this.shipped_time
  }

  public get cancelledTime() {
    return this.cancelled_time
  }

  public get closedTime() {
    return this.closed_time
  }
}

export enum OrderRefundKind {
  OnlyRefund = "only_refund", ReturnRefund = "return_refund"
}

export enum OrderRefundStatus {
  Incomplete = "incomplete",
  Applying = "applying",
  Disapproved = "disapproved",
  Pending = "pending",
  Succeeded = "succeeded",
  Failed = "failed"
}

export class OrderRefundItem {
  public id: string
  private item_id: string
  private item_status: string
  public name: string
  public amount: string
  private image_url: string
  public notes: string

  public get itemId() {
    return this.item_id
  }

  public set itemId(itemId) {
    this.item_id = itemId
  }

  public get itemStatus() {
    return this.item_status
  }

  public set itemStatus(itemStatus) {
    this.item_status = itemStatus
  }

  public get imageUrl() {
    return this.image_url
  }

  public set imageUrl(imageUrl) {
    this.image_url = imageUrl
  }
}

export class OrderRefund {
  public id: string
  private order_id: string
  public kind: OrderRefundKind
  public status: OrderRefundStatus
  public items: OrderRefundItem[] = []
  public reason: string
  public attachments: string[]
  private total_amount: number
  private disapproval_reason: string
  private fail_reason: string
  private applied_time: string
  private approved_time: string
  private disapproved_time: string
  private succeeded_time: string

  public get orderId() {
    return this.order_id
  }

  public get totalAmount() {
    return this.total_amount
  }

  public get disapprovalReason() {
    return this.disapproval_reason
  }

  public get failReason() {
    return this.fail_reason
  }

  public get appliedTime() {
    return this.applied_time
  }

  public get approvedTime() {
    return this.approved_time
  }

  public get disapprovedTime() {
    return this.disapproved_time
  }

  public get succeededTime() {
    return this.succeeded_time
  }
}

export function assignOrder(order: Order) {
  return _.assign(new Order(), order, {
    shippingAddress: _.assign(new Address(), order.shipping_address),
    items: _.map(order.items, item => _.assign(new OrderItem(), item)),
    shipments: _.map(order.shipments, assignOrderShipment),
    refunds: _.map(order.refunds, assignOrderRefund),
  })
}

export class OrderItemRequest {
  id: string
  discountAmount ?: number
  discountShippingCost ?: number
}

export class OrderRequest {
  id: string
  staffNotes ?: string
  discountAmount ?: number
  items?: OrderItemRequest[]
}

export class OrderQuery {

  private params = {}

  public page(page?: string | number): OrderQuery {
    if (page) {
      this.params["page"] = page
    }
    return this
  }

  public limit(limit?: string | number): OrderQuery {
    if (limit) {
      this.params["limit"] = limit
    }
    return this
  }

  public ids(ids?: string | string[]): OrderQuery {
    if (ids) {
      this.params["ids"] = ids
    }
    return this
  }

  public customerId(customerId: string): OrderQuery {
    this.params["customer_id"] = customerId
    return this
  }

  public statuses(statuses?: OrderStatus[] | string): OrderQuery {
    if (statuses) {
      this.params["statuses"] = statuses
    }
    return this
  }

  public refundStatuses(statuses?: OrderStatus[] | string): OrderQuery {
    if (statuses) {
      this.params["refund_statuses"] = statuses
    }
    return this
  }

  public placedTimeMin(placedTimeMin: string): OrderQuery {
    if (placedTimeMin) {
      this.params["placed_time_min"] = placedTimeMin
    }
    return this
  }

  public placedTimeMax(placedTimeMax: string): OrderQuery {
    if (placedTimeMax) {
      this.params["placed_time_max"] = placedTimeMax
    }
    return this
  }

  public storeId(storeId: string): OrderQuery {
    this.params["store_id"] = storeId
    return this
  }

  public list(): Promise<PageList<Order>> {
    return httpClient.get("/v1/orders", {
      params: this.params,
    }).then(({ data }) => {
      return _.assign(new PageList<Order>(), data, {
        elements: _.map(data.elements, assignOrder),
      })
    })
  }

  public count(): Promise<number> {
    return httpClient.get("/v1/orders/count", {
      params: this.params,
    }).then(({ data }) => data)
  }
}

export class OrderServiceV1 {

  public static createOrders(orders: Order[]): Promise<Order[]> {
    return httpClient.post("/v1/orders/batch", orders)
      .then(({ data }) => _.map(data, assignOrder))
  }

  public static saveOrder(request: OrderRequest): Promise<void> {
    return httpClient.patch(`/v1/orders/${request.id}`, {
      ...(_.isEmpty(request.staffNotes) ? {} : { staff_notes: request.staffNotes }),
      ...(_.isEmpty(request.discountAmount) ? {} : { discount_amount: request.discountAmount }),
      ...(_.isEmpty(request.items) ? {} : {
        items: _.map(request.items, item => ({
          id: item.id,
          ...(_.isUndefined(item.discountAmount) ? {} : { discount_amount: item.discountAmount }),
          ...(_.isUndefined(item.discountShippingCost) ? {} : { discount_shipping_cost: item.discountShippingCost }),
        })),
      }),
    })
  }

  public static cancelOrder(orderId: string, reason: string): Promise<void> {
    return httpClient.post(`/v1/orders/${orderId}/cancel`, { reason })
  }

  public static receiptOrder(orderId: string): Promise<void> {
    return httpClient.post(`/v1/orders/${orderId}/receipt`)
  }

  public static getOrder(orderId: string): Promise<Order> {
    return httpClient.get(`/v1/orders/${orderId}`)
      .then(({ data }) => data)
      .then(assignOrder)
  }

  public static createOrderQuery() {
    return new OrderQuery()
  }

  public static getOrderShipments(orderId: string): Promise<Shipment[]> {
    return httpClient.get(`/v1/orders/${orderId}/shipments`)
      .then(({ data: shipments }) => _.map(shipments, assignOrderShipment))
  }

  public static addOrderShipment(orderId: string, shipment: Shipment): Promise<Shipment> {
    return httpClient.post(`/v1/orders/${orderId}/shipments`, shipment)
      .then(({ data }) => _.assign(new Shipment(), data))
  }

  public static saveOrderShipments(orderId: string, requests: ShipmentRequest[]): Promise<void> {
    return httpClient.patch(`/v1/orders/${orderId}/shipments/batch`, requests)
  }

  public static startOrderPayment(orderPayment: OrderPayment): Promise<Payment> {
    return httpClient.post(`/v1/orders/payments`, orderPayment)
      .then(({ data }) => _.assign(new Payment(), data))
  }

  public static applyOrderRefund(orderId: string, refund: OrderRefund): Promise<OrderRefund> {
    return httpClient.post(`/v1/orders/${orderId}/refunds`, refund)
      .then(({ data }) => data)
      .then(assignOrderRefund)
  }

  public static cancelOrderRefund(orderId: string, refundId: string): Promise<void> {
    return httpClient.delete(`/v1/orders/${orderId}/refunds/${refundId}/cancel`)
  }

  public static getOrderRefund(orderId: string, refundId: string): Promise<OrderRefund> {
    return httpClient.get(`/v1/orders/${orderId}/refunds/${refundId}`)
      .then(({ data }) => data)
      .then(assignOrderRefund)
  }
}
